package com.daxue.configtest.base;

import com.daxue.configtest.exception.impl.BadArgumentException;
import com.daxue.configtest.exception.impl.ConflictException;
import com.daxue.configtest.exception.impl.NotFoundException;
import com.daxue.configtest.others.CollectionsUtils;
import com.daxue.configtest.others.Entity;
import com.daxue.configtest.others.Nullable;
import com.daxue.configtest.others.Unmodifiable;
import lombok.extern.slf4j.Slf4j;
import org.jooq.DSLContext;
import org.jooq.DeleteQuery;
import org.jooq.Field;
import org.jooq.InsertQuery;
import org.jooq.OrderField;
import org.jooq.Table;
import org.jooq.UpdatableRecord;
import org.jooq.UpdateQuery;
import org.jooq.exception.TooManyRowsException;
import org.springframework.beans.factory.BeanInitializationException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.TransactionException;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.springframework.util.StringUtils;

import javax.annotation.PostConstruct;
import java.sql.Timestamp;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

import static com.daxue.configtest.others.AlertUtils.alertMessage;
import static com.daxue.configtest.others.AlertUtils.alertNull;
import static com.daxue.configtest.others.AlertUtils.isTrue;
import static com.daxue.configtest.others.CollectionsUtils.listOf;
import static com.daxue.configtest.others.PageWalker.INIT_PAGEABLE;
import static java.util.Objects.isNull;
import static java.util.Objects.nonNull;
import static java.util.stream.Collectors.toList;
import static org.jooq.SortOrder.ASC;
import static org.jooq.SortOrder.DESC;
import static org.jooq.impl.DSL.name;
import static org.springframework.util.ReflectionUtils.findField;

/**
 * @author daxue0929
 * @date 2023/3/14
 */

@Slf4j
public abstract class BaseRepository<R extends UpdatableRecord, E extends BaseEntity> implements Repository<E> {

    protected DataSourceTransactionManager txMgr;
    protected DSLContext dslContext;
    private final Map<Class<Table<R>>, Table<R>> tableMap = new ConcurrentHashMap<>(16);

    @Autowired
    public void setTxMgr(final DataSourceTransactionManager txMgr) {
        this.txMgr = txMgr;
    }

    @Autowired
    public void setDslContext(final DSLContext dslContext) {
        this.dslContext = dslContext;
    }

    @PostConstruct
    private Table<R> init() {
        try {
            if (getEntityType().isAnnotationPresent(Entity.class)) {
                return getTable(); //有返回值也没啥用，容器会丢掉的。
            } else {
                throw new BeanInitializationException(
                    alertMessage("Entity %s must be annotated by %s",
                        getEntityType().getName(), Entity.class.getName()
                    )
                );
            }
        } catch (IllegalArgumentException e) {
            throw new BeanInitializationException(alertMessage(
                "Failed to get table instance of %s", getAnnotation().table().getName()), e);
        }
    }

    @Override
    public void preCheck(final E entity) throws BadArgumentException {
        if (!entity.getId().startsWith(getAnnotation().prefix())) {
            throw new BadArgumentException("id prefix wrong");
        }
    }


    final protected E create0(final E entity) throws BadArgumentException, ConflictException {
        if (StringUtils.hasText(entity.getId())) {
            if (!supportMagicId()) {
                throw new BadArgumentException("magic id is not allowed");
            }
        } else {
            entity.setId(genUUID());
        }
        final Timestamp currentTime = new Timestamp(System.currentTimeMillis());
        entity.setCreatedOn(currentTime);
        entity.setModifiedOn(currentTime);
        preCheck(entity);

        try {
            final InsertQuery<R> insertQuery = dslContext.insertQuery(getTable());
            insertQuery.setRecord(applySelectiveInsert(convert(entity)));
            insertQuery.setReturning();

            isTrue(insertQuery.execute() > 0, () -> new ConflictException("Insert 0 rows"));
            return Optional.ofNullable(insertQuery.getReturnedRecord()).map(this::convert).orElse(entity);
        } catch (DataIntegrityViolationException e) {
            throw new ConflictException(e);
        }
    }


    final protected Optional<E> read0(final Sort sort, final QueryConditions conditions) throws ConflictException {
        try {
            return dslContext
                .selectFrom(getTable())
                .where(conditions)
                .orderBy(orderFields(sort))
                .fetchOptional()
                .map(this::convert);
        }catch (DataIntegrityViolationException | TooManyRowsException e) {
            throw new ConflictException(e);
        }
    }

    final protected Page<E> readAll0(final Pageable pageable, final QueryConditions condition) throws ConflictException {
        final Pageable page = pageable.isUnpaged() ? INIT_PAGEABLE : pageable;
        final TransactionStatus txStatus = txMgr.getTransaction(new DefaultTransactionDefinition());
        try {
            final List<E> results = dslContext
                .selectFrom(getTable())
                .where(condition)
                .orderBy(orderFields(page.getSort()))
                .offset((int) page.getOffset())
                .limit(page.getPageSize())
                .fetchInto(getEntityType());
            final int total = dslContext.fetchCount(getTable(), condition.asCondition());
            return new PageImpl<>(results, page, total);
        } catch (DataIntegrityViolationException | TransactionException e) {
            throw new ConflictException(e);
        } finally {
            txMgr.commit(txStatus);
        }
    }

    final protected E update0(final E entity, final QueryConditions conditions) throws
        NotFoundException, BadArgumentException, ConflictException{
        entity.setModifiedOn(new Timestamp(System.currentTimeMillis()));
        try {
            final UpdateQuery<R> updateQuery = dslContext.updateQuery(getTable());
            updateQuery.setRecord(applySelectiveUpdate(convert(entity)));
            updateQuery.addConditions(conditions);
            updateQuery.setReturning();
            isTrue(updateQuery.execute() > 0, this::notFoundException);
            return Optional.ofNullable(updateQuery.getReturnedRecord()).map(this::convert).orElse(entity);
        } catch (DataIntegrityViolationException e) {
            throw new ConflictException(e);
        }
    }

    final protected E delete0(final QueryConditions conditions) throws NotFoundException, ConflictException {
        try {
            final DeleteQuery<R> deleteQuery = dslContext.deleteQuery(getTable());
            deleteQuery.addConditions(conditions);
            deleteQuery.setReturning();
            isTrue(deleteQuery.execute() > 0, this::notFoundException);
            return Optional.ofNullable(deleteQuery.getReturnedRecord()).map(this::convert).orElse(null);
        } catch (DataIntegrityViolationException e) {
            throw new ConflictException(e);
        }
    }

    final protected Page<E> deleteAll0(final QueryConditions conditions) throws ConflictException {
        try {
            final DeleteQuery<R> deleteQuery = dslContext.deleteQuery(getTable());
            deleteQuery.addConditions(conditions);
            deleteQuery.setReturning();
            deleteQuery.execute();
            List<E> results = Optional.ofNullable(deleteQuery.getReturnedRecords())
                .map(r -> r.stream().map(this::convert).collect(Collectors.toList()))
                .orElseGet(CollectionsUtils::listOf);
            return new PageImpl<>(results);
        } catch (DataIntegrityViolationException e) {
            throw new ConflictException(e);
        }
    }

    protected Collection<? extends OrderField<?>> orderFields(Sort sort) {
        return sort.isUnsorted()
            ? listOf(DAO.Fields.DEFAULT_SORT_FIELD)
            : sort.stream()
            .map(order -> getTable()
                .field(name(order.getProperty()))
                .sort(order.isAscending() ? ASC : DESC))
            .collect(toList());
    }





    private R convert(E entity) {
        return dslContext.newRecord(getTable(), entity);
    }

    private E convert(R record) {
        return record.into(getEntityType());
    }


    private R applySelectiveUpdate(R record) {
        for (final Field<?> field : record.fields()) {
            final java.lang.reflect.Field entityField = findField(getEntityType(), field.getName());
            if (isNull(entityField)) {
                record.changed(field, false);
                log.warn("Entity field is not matching with record field('{}').", field.getName());
            } else if (entityField.isAnnotationPresent(Unmodifiable.class)) {
                record.changed(field, false);
            } else if (isNull(record.getValue(field)) && !entityField.isAnnotationPresent(Nullable.class)) {
                record.changed(field, false);
            }
        }
        return record;
    }

    private R applySelectiveInsert(R record) throws BadArgumentException {
        for (final Field<?> field : record.fields()) {
            final java.lang.reflect.Field entityField = findField(getEntityType(), field.getName());
            if (entityField == null ||
                isNull(record.getValue(field)) && !entityField.isAnnotationPresent(Nullable.class)) {
                throw new BadArgumentException(alertNull(field.getName()));
            }
            record.changed(field, nonNull(record.getValue(field)));
        }
        return record;
    }

    private String genUUID() {
        return generateUUID(getAnnotation().prefix(), true);
    }

    public static String generateUUID(String prefix, boolean replace) {
        String idStr = UUID.randomUUID().toString().replace(replace ? "-" : "", "");
        if (StringUtils.isEmpty(prefix)) {
            return idStr;
        } else {
            return prefix.endsWith("-") ? prefix.concat(idStr) : prefix.concat("-").concat(idStr);
        }
    }


    final protected Table<R> getTable() {
        Class<Table<R>> table = (Class<Table<R>>) getAnnotation().table();
        Table<R> table1 = null;
        try {
            table1 = table.newInstance();
        } catch (InstantiationException | IllegalAccessException e) {
            e.printStackTrace();
        }
        return table1;
    }

    private Entity getAnnotation() {
        return AnnotationUtils.findAnnotation(getEntityType(), Entity.class);
    }

    final protected Class<E> getEntityType() {
        return (Class<E>) ResolvableType
            .forType(this.getClass().getGenericSuperclass())
            .getGeneric(1).getRawClass();
    }

    @SuppressWarnings("unchecked")
    protected Field<String> primaryKey() {
        return (Field<String>) getTable().getPrimaryKey().getFieldsArray()[0];
    }

    final protected NotFoundException notFoundException() {
        return new NotFoundException(String.format("[%s] Not found", getTable().getName()));
    }
}
